<template>
  <div class="range-wrapper" ref="range" @click.self="rangeClick">
    <div v-if="showStops">
      <div class="stops" v-for="(item, index) in stops" :key="index" :style="{ 'left': item + '%' }" @click.self="rangeClick">
        <p class="stop-content">{{customStops.length === 0 ? item * max / 100 : customStops[index]}}</p>
      </div>
    </div>
    <div class="bar" :style="barStyle" @click.self="rangeClick"></div>
    <template v-if="range">
      <div class="button-wrap" :style="{left: firstPosition + '%'}" @mousedown="onFirstButtonDown" @touchstart="onFirstButtonDown">
        <div :class="button1Classes"></div>
      </div>
      <div class="button-wrap" :style="{left: secondPosition + '%'}" @mousedown="onSecondButtonDown" @touchstart="onSecondButtonDown">
        <div :class="button2Classes"></div>
      </div>
    </template>
    <template v-else>
      <div class="button-wrap" :style="{left: singlePosition + '%'}" @mousedown="onSingleButtonDown" @touchstart="onSingleButtonDown">
        <div :class="buttonClasses"></div>
      </div>
    </template>
  </div>
</template>
<script>
import { on, off } from '@/assets/js/dom'

export default {
  name: 'Range',
  props: {
    min: {
      type: Number,
      default: 0
    },
    max: {
      type: Number,
      default: 100
    },
    step: {
      type: Number,
      default: 1
    },
    range: {
      type: Boolean,
      default: false
    },
    value: {
      type: [Number, Array],
      default: 0
    },
    disabled: {
      type: Boolean,
      default: false
    },
    showStops: {
      type: Boolean,
      default: false
    },
    customStops: {
      type: Array,
      default: []
    }
  },
  data() {
    return {
      currentValue: this.value,
      dragging: false,
      firstDragging: false,
      secondDragging: false,
      startX: 0,
      currentX: 0,
      startPos: 0,
      newPos: null,
      oldSingleValue: this.value,
      oldFirstValue: this.value[0],
      oldSecondValue: this.value[1],
      singlePosition: (this.value - this.min) / (this.max - this.min) * 100,
      firstPosition: (this.value[0] - this.min) / (this.max - this.min) * 100,
      secondPosition: (this.value[1] - this.min) / (this.max - this.min) * 100
    }
  },
  watch: {
    value(val) {
      this.currentValue = val
      this.updateValue(val)
    }
  },
  computed: {
    buttonClasses() {
      return [
        `range-button`,
        {
          [`range-button-dragging`]: this.dragging
        }
      ]
    },
    button1Classes() {
      return [
        `range-button`,
        {
          [`range-button-dragging`]: this.firstDragging
        }
      ]
    },
    button2Classes() {
      return [
        `range-button`,
        {
          [`range-button-dragging`]: this.secondDragging
        }
      ]
    },
    barStyle() {
      let style

      if (this.range) {
        style = {
          width: (this.currentValue[1] - this.currentValue[0]) / (this.max - this.min) * 100 + '%',
          left: (this.currentValue[0] - this.min) / (this.max - this.min) * 100 + '%'
        }
      } else {
        style = {
          width: (this.currentValue - this.min) / (this.max - this.min) * 100 + '%'
        }
      }

      return style
    },
    stops() {
      let stopCount = (this.max - this.min) / this.step
      let result = []
      let stepWidth = 100 * this.step / (this.max - this.min)
      for (let i = 0; i <= stopCount; i++) {
        result.push(i * stepWidth)
      }
      return result
    },
    rangeWidth() {
      return parseInt(this.$refs.range.offsetWidth, 10)
    }
  },
  methods: {
    updateValue(val, init = false) {
      if (this.range) {
        let value = [...val]
        if (init) {
          if (value[0] > value[1]) {
            value = [this.min, this.max]
          }
        } else {
          if (value[0] > value[1]) {
            value[0] = value[1]
          }
        }
        if (value[0] < this.min) {
          value[0] = this.min
        }
        if (value[0] > this.max) {
          value[0] = this.max
        }
        if (value[1] < this.min) {
          value[1] = this.min
        }
        if (value[1] > this.max) {
          value[1] = this.max
        }
        if (this.value[0] === value[0] && this.value[1] === value[1]) {
          this.setFirstPosition(this.currentValue[0])
          this.setSecondPosition(this.currentValue[1])
          return
        }

        this.currentValue = value
        this.setFirstPosition(this.currentValue[0])
        this.setSecondPosition(this.currentValue[1])
      } else {
        if (val < this.min) {
          this.currentValue = this.min
        }
        if (val > this.max) {
          this.currentValue = this.max
        }
        this.setSinglePosition(this.currentValue)
      }
    },
    rangeClick(event) {
      if (this.disabled) return
      const currentX = event.clientX || event.touches[0].clientX
      const rangeOffsetLeft = this.$refs.range.getBoundingClientRect().left
      const newPos = (currentX - rangeOffsetLeft) / this.rangeWidth * 100

      if (this.range) {
        let type = ''
        if (newPos <= this.firstPosition) {
          type = 'First'
        } else if (newPos >= this.secondPosition) {
          type = 'Second'
        } else {
          if ((newPos - this.firstPosition) <= (this.secondPosition - newPos)) {
            type = 'First'
          } else {
            type = 'Second'
          }
        }
        this[`change${type}Position`](newPos)
      } else {
        this.changeSinglePosition(newPos)
      }
    },
    // for single use
    onSingleButtonDown(event) {
      if (this.disabled) return
      event.preventDefault()
      this.onSingleDragStart(event)
      on(window, 'mousemove', this.onSingleDragging)
      on(window, 'mouseup', this.onSingleDragEnd)
      on(window, 'touchmove', this.onSingleDragging)
      on(window, 'toucnend', this.onSingleDragEnd)
    },
    onSingleDragStart(event) {
      this.dragging = false
      this.startX = event.clientX || event.touches[0].clientX
      this.startPos = parseInt(this.singlePosition, 10)
    },
    onSingleDragging(event) {
      this.dragging = true
      if (this.dragging) {
        this.currentX = event.clientX || event.touches[0].clientX
        const diff = (this.currentX - this.startX) / this.rangeWidth * 100
        this.newPos = this.startPos + diff
        this.changeSinglePosition(this.newPos)
      }
    },
    onSingleDragEnd() {
      if (this.dragging) {
        this.dragging = false
        this.changeSinglePosition(this.newPos)
      }
      off(window, 'mousemove', this.onSingleDragging)
      off(window, 'mouseup', this.onSingleDragEnd)
      off(window, 'touchmove', this.onSingleDragging)
      off(window, 'touchend', this.onSingleDragEnd)
    },
    changeSinglePosition(newPos) {
      if (newPos < 0) {
        newPos = 0
      } else if (newPos > 100) {
        newPos = 100
      }
      const lengthPerStep = 100 / ((this.max - this.min) / this.step)
      const steps = Math.round(newPos / lengthPerStep)

      this.currentValue = Math.round(steps * lengthPerStep * (this.max - this.min) * 0.01 + this.min)
      this.setSinglePosition(this.currentValue)
      if (!this.dragging) {
        if (this.currentValue !== this.oldSingleValue) {
          this.$emit('on-change', this.currentValue)
          this.oldSingleValue = this.currentValue
        }
      }
    },
    setSinglePosition(val) {
      this.singlePosition = (val - this.min) / (this.max - this.min) * 100
    },
    // for range use first
    onFirstButtonDown(event) {
      if (this.disabled) return
      event.preventDefault()
      this.onFirstDragStart(event)
      on(window, 'mousemove', this.onFirstDragging)
      on(window, 'mouseup', this.onFirstDragEnd)
      // 适配移动端
      on(window, 'touchmove', this.onFirstDragging)
      on(window, 'touchend', this.onFirstDragEnd)
    },
    onFirstDragStart(event) {
      this.firstDragging = false
      this.startX = event.clientX || event.touches[0].clientX
      this.startPos = parseInt(this.firstPosition, 10)
    },
    onFirstDragging(event) {
      this.firstDragging = true
      if (this.firstDragging) {
        this.currentX = event.clientX || event.touches[0].clientX
        const diff = (this.currentX - this.startX) / this.rangeWidth * 100
        this.newPos = this.startPos + diff
        this.changeFirstPosition(this.newPos)
      }
    },
    onFirstDragEnd() {
      if (this.firstDragging) {
        this.firstDragging = false
        this.changeFirstPosition(this.newPos)
      }
      off(window, 'mousemove', this.onFirstDragging)
      off(window, 'mouseup', this.onFirstDragEnd)
      off(window, 'touchmove', this.onFirstDragging)
      off(window, 'touchend', this.onFirstDragEnd)
    },
    changeFirstPosition(newPos) {
      if (newPos < 0) {
        newPos = 0
      } else if (newPos > this.secondPosition) {
        newPos = this.secondPosition
      }
      const lengthPerStep = 100 / ((this.max - this.min) / this.step)
      const steps = Math.round(newPos / lengthPerStep)

      this.currentValue = [Math.round(steps * lengthPerStep * (this.max - this.min) * 0.01 + this.min), this.currentValue[1]]
      this.setFirstPosition(this.currentValue[0])
      if (!this.firstDragging) {
        if (this.currentValue[0] !== this.oldFirstValue) {
          this.$emit('on-change', this.currentValue)
          this.oldFirstValue = this.currentValue[0]
        }
      }
    },
    setFirstPosition(val) {
      this.firstPosition = (val - this.min) / (this.max - this.min) * 100
    },
    // for range use second
    onSecondButtonDown(event) {
      if (this.disabled) return
      event.preventDefault()
      this.onSecondDragStart(event)
      on(window, 'mousemove', this.onSecondDragging)
      on(window, 'mouseup', this.onSecondDragEnd)
      on(window, 'touchmove', this.onSecondDragging)
      on(window, 'touchend', this.onSecondDragEnd)
    },
    onSecondDragStart(event) {
      this.secondDragging = false
      this.startX = event.clientX || event.touches[0].clientX
      this.startPos = parseInt(this.secondPosition, 10)
    },
    onSecondDragging(event) {
      this.secondDragging = true
      if (this.secondDragging) {
        this.currentX = event.clientX || event.touches[0].clientX
        const diff = (this.currentX - this.startX) / this.rangeWidth * 100
        this.newPos = this.startPos + diff
        this.changeSecondPosition(this.newPos)
      }
    },
    onSecondDragEnd() {
      if (this.secondDragging) {
        this.secondDragging = false
        this.changeSecondPosition(this.newPos)
        off(window, 'mousemove', this.onSecondDragging)
        off(window, 'mouseup', this.onSecondDragEnd)
        off(window, 'touchmove', this.onSecondDragging)
        off(window, 'touchend', this.onSecondDragEnd)
      }
    },
    changeSecondPosition(newPos) {
      if (newPos > 100) {
        newPos = 100
      } else if (newPos < this.firstPosition) {
        newPos = this.firstPosition
      }
      const lengthPerStep = 100 / ((this.max - this.min) / this.step)
      const steps = Math.round(newPos / lengthPerStep)

      this.currentValue = [this.currentValue[0], Math.round(steps * lengthPerStep * (this.max - this.min) * 0.01 + this.min)]
      this.setSecondPosition(this.currentValue[1])
      if (!this.secondDragging) {
        if (this.currentValue[1] !== this.oldSecondValue) {
          this.$emit('on-change', this.currentValue)
          this.oldSecondValue = this.currentValue[1]
        }
      }
    },
    setSecondPosition(val) {
      this.secondPosition = (val - this.min) / (this.max - this.min) * 100
    }
  },
  mounted() {
    if (this.range) {
      const isArray = Array.isArray(this.currentValue)
      if (!isArray || (isArray && this.currentValue.length !== 2) || (isArray && (isNaN(this.currentValue[0]) || isNaN(this.currentValue[1])))) {
        this.currentValue = [this.min, this.max]
      } else {
        this.updateValue(this.currentValue, true)
      }
    } else {
      if (typeof this.currentValue !== 'number') {
        this.currentValue = this.min
      }
      this.updateValue(this.currentValue)
    }
  }
}
</script>

<style lang="stylus" scoped>
@import '~@/assets/css/variable.styl';

.range-wrapper {
  line-height: normal;
  height: 4px;
  margin-left: 15px;
  margin-right: 15px;
  margin-top: 16px;
  margin-bottom: 26px;
  background-color: #e5e5e5;
  border-radius: 3px;
  vertical-align: middle;
  position: relative;
  cursor: pointer;

  .button-wrap {
    width: 8px;
    height: 8px;
    text-align: center;
    background-color: transparent;
    position: absolute;
    top: -10px;
    transform: translateX(-125%);
  }

  .range-button {
    width: 100%;
    height: 100%;
    border: 8px solid tint(#fff, 20%);
    border-radius: 50%;
    background-color: $color-theme;
    transition: all 0.2s linear;
    box-shadow: 0 0 10px #aaa;

    &:hover, .range-dragging {
      border-color: $color-theme;
      transform: scale(1.5);
    }

    &:hover {
      cursor: grab;
    }

    .range-dragging, .range-dragging:hover {
      cursor: grabbing;
    }
  }

  .bar {
    height: 4px;
    background: tint($color-theme, 20%);
    border-radius: 3px;
    position: absolute;
  }

  .stops {
    position: absolute;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    top: -2px;
    background-color: #ccc;
    transform: translateX(-50%);

    .stop-content {
      width: fit-content;
      transform: translateX(-50%);
      color: $color-text-2;
      font-size: $font-size-small;
      text-align: center;
      white-space: nowrap;
      margin-top: 16px;
    }

    &:first-child .stop-content, &:last-child .stop-content {
      transform: translateX(-25%);
    }
  }
}
</style>
